﻿using System;
using System.Collections.Generic;
using System.IO;
using SR = System.Reflection;
using Mono.Cecil;
using Mono.Cecil.Cil;
using Mono.Cecil.Rocks;

namespace LibParasInject
{
    public class Injector
    {
        public void inject (
            string tracerPath, 
            string dirOut, 
            List<string> bins, 
            List<string> resolverSearchDirs
        )
        {
            if (! File.Exists(tracerPath))
                throw new Exception("el path del tracer no existe");

            var fTablePath = Path.Combine(dirOut, "ftable.txt");

            if (! Directory.Exists(dirOut))
                Directory.CreateDirectory(dirOut);

            if (File.Exists(fTablePath))
                File.Delete(fTablePath);

            var resolver = new DefaultAssemblyResolver();
            if (resolverSearchDirs != null) {
                foreach (string resolverSearchDir in resolverSearchDirs)
                    resolver.AddSearchDirectory(resolverSearchDir);
            }
            var readerParameters = new ReaderParameters {
                AssemblyResolver = resolver
            };

            int methodId = 0;
            foreach (string bin in bins) {
                AssemblyDefinition asmDef = AssemblyDefinition.ReadAssembly(
                    bin, readerParameters
                );
                var partialFuncTable = transformAssembly(
                    asmDef, tracerPath, ref methodId
                );
                using (StreamWriter sw = File.AppendText(fTablePath)) {
                    foreach (FuncTableFileItem item in partialFuncTable)
                        sw.WriteLine(item.ToString());
                    sw.Close();
                }
                string newBinFileName = Path.GetFileName(bin);
                string pathNewBin = Path.Combine(dirOut, newBinFileName);
                asmDef.Write(pathNewBin);
            }

            // copio el tracer a la carpeta "out"
            string tracerFileName = Path.GetFileName(tracerPath);
            string tracerNewPath = Path.Combine(dirOut, tracerFileName);
            File.Copy(tracerPath, tracerNewPath, true);
        }

        public static bool isValidAssembly (string sFileName)
        {
            try
            {
                using (FileStream fs = File.OpenRead(sFileName)) {
                    if ((fs.ReadByte() != 'M') || (fs.ReadByte() != 'Z')) {
                        fs.Close();
                        return false;
                    }
                    fs.Close();
                }
                // http://msdn.microsoft.com/en-us/library/ms173100.aspx
                object foo = SR.AssemblyName.GetAssemblyName(sFileName);
                return true;
            }
            catch 
            {
                return false;
            }
        }

        #region para borrar...
        //public static void inject (string [] args)
        //{
        //    /*  por ejemplo...
        //        "..\..\..\ParasiTracer\ParasiTracerCF\bin\Debug\ParasiTracer.dll"
        //        "C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\ParasiOut"
        //        "C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\TecnoBus.Presentacion.exe"
        //        "C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\TecnoBus.Comun.dll"
        //        "C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\TecnoBus.Comunicacion.dll"
        //        "C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\TecnoBus.Dispositivos.dll"
        //     */

        //    _tracerPath = args[0];

        //    if (!File.Exists(_tracerPath))
        //        throw new Exception("el path del tracer no existe");

        //    var dirOut = args[1];
        //    var fTablePath = Path.Combine(dirOut, "ftable.txt");
        //    var assemblyPaths = new List<string>();

        //    for (int i = 2; i < args.Length; i++)
        //        assemblyPaths.Add(args[i]);

        //    if (!Directory.Exists(dirOut))
        //        Directory.CreateDirectory(dirOut);

        //    if (File.Exists(fTablePath))
        //        File.Delete(fTablePath);

        //    var resolver = new DefaultAssemblyResolver();
        //    resolver.AddSearchDirectory(
        //        @"C:\FuentesCSharp\TecnoBusPortable\TecnoBus.Presentacion\bin\Debug\"
        //    );

        //    var readerParameters = new ReaderParameters
        //    {
        //        AssemblyResolver = resolver
        //    };

        //    int methodId = 0;
        //    foreach (string assemblyPath in assemblyPaths)
        //    {
        //        var assembly = AssemblyDefinition.ReadAssembly(
        //            assemblyPath, readerParameters
        //        );

        //        var partialFuncTable = transformAssembly(assembly, ref methodId);

        //        using (StreamWriter sw = File.AppendText(fTablePath))
        //        {
        //            foreach (FuncTableFileItem item in partialFuncTable)
        //                sw.WriteLine(item.ToString());

        //            sw.Close();
        //        }

        //        assembly.Write(
        //            Path.Combine(
        //                dirOut, Path.GetFileName(assemblyPath)));
        //    }

        //    // copio el tracer a la carpeta "out"
        //    File.Copy(
        //        _tracerPath,
        //        Path.Combine(dirOut, Path.GetFileName(_tracerPath)),
        //        true
        //    );
        //}
        #endregion

        void transformMethodStart(
            TypeDefinition type,
            MethodDefinition md,
            int methodId,
            ParasiRefs tracerReferences
        )
        {
            var worker = md.Body.GetILProcessor();
            var start = md.Body.getStart();

            // empujo id del metodo
            md.Body.injectBefore(
                start,
                worker.Create(OpCodes.Ldc_I4, methodId)
            );

            //if (md.Parameters.Count == 0)
            {
                md.Body.injectBefore(
                    start,
                    worker.Create(OpCodes.Call, tracerReferences["inFunc"])
                );
            }
            //else
            //{
            //    // llamo a inFuncWithParams, empujo un 1, si inFuncWithParams(..) != 1, salto!
            //    md.Body.injectBefore(
            //        start,
            //        worker.Create(OpCodes.Call, tracerReferences["inFuncWithParams"]),
            //        worker.Create(OpCodes.Ldc_I4_1),
            //        worker.Create(OpCodes.Bne_Un, start)
            //    );

            //    foreach (ParameterDefinition param in md.Parameters)
            //    {
            //        int paramIndex = param.Index +
            //            (md.IsStatic ? 0 : 1);

            //        MetadataType paramMetadataType =
            //            param.ParameterType.MetadataType;

            //        switch (paramMetadataType)
            //        {
            //            case MetadataType.Boolean:
            //            case MetadataType.Char:
            //            case MetadataType.SByte:
            //            case MetadataType.Byte:
            //            case MetadataType.Int16:
            //            case MetadataType.UInt16:
            //            case MetadataType.Int32:
            //            case MetadataType.UInt32:
            //            case MetadataType.Int64:
            //            case MetadataType.UInt64:
            //            case MetadataType.Single:
            //            case MetadataType.Double:
            //            case MetadataType.String:
            //            case MetadataType.Object:
            //            case MetadataType.Class:
            //            case MetadataType.Array:
            //                //case MetadataType.GenericInstance: // <- tira error porque hay que boxear ciertas instancias genericas como System.Nullable ("?") INVESTIGAR!
            //                Console.WriteLine("{0} :: {1}", type.Name, md.Name);
            //                Console.WriteLine();
            //                md.Body.injectBefore(
            //                    start,
            //                    worker.createLdargX(paramIndex),
            //                    worker.Create(OpCodes.Call, tracerReferences[paramMetadataType])
            //                );
            //                break;
            //            case MetadataType.ValueType:
            //                md.Body.injectBefore(
            //                    start,
            //                    worker.createLdargX(paramIndex),
            //                    worker.Create(OpCodes.Box, param.ParameterType),
            //                    worker.Create(OpCodes.Call, tracerReferences["objectParam"])
            //                );
            //                break;

            //            #region generics y refs
            //            //case MetadataType.MVar: // T
            //            //    ///Console.ForegroundColor = ConsoleColor.Red;
            //            //    ///Console.WriteLine(param.ParameterType);
            //            //    ///Console.WriteLine(" resolveto-> {0}", param.ParameterType);
            //            //    ///Console.WriteLine(" metadatat-> {0}", param.ParameterType.MetadataType);
            //            //    break;
            //            //case MetadataType.ByReference:
            //            //    /// si es "out" no se que hacer...
            //            //    /// (se puede verificar si es out con 
            //            //    /// "param.Attributes == ParameterAttributes.Out"
            //            //    /// 
            //            //    /// cargo con ldind.[opcion]
            //            //    /// si su tipo resuelto es un ValueType tengo
            //            //    /// que boxearlo...
            //            //    ///
            //            //    ///ByReferenceType byref = (ByReferenceType)param.ParameterType;
            //            //    ///if (byref.ElementType.IsGenericParameter)
            //            //    ///{
            //            //    ///    Console.ForegroundColor = ConsoleColor.Cyan;
            //            //    ///    GenericParameter gp = (GenericParameter)byref.ElementType;
            //            //    ///    Console.WriteLine(byref);
            //            //    ///    Console.WriteLine("metadatat-> {0}" , byref.MetadataType);
            //            //    ///    Console.WriteLine(" resolveto-> {0}", gp);
            //            //    ///    Console.WriteLine(" metadatat-> {0}", gp.MetadataType);
            //            //    ///}
            //            //    ///else
            //            //    ///{
            //            //    ///    if (byref.ElementType.IsGenericInstance)
            //            //    ///    {
            //            //    ///        Console.ForegroundColor = ConsoleColor.Yellow;
            //            //    ///    }
            //            //    ///    else
            //            //    ///    {
            //            //    ///        Console.ForegroundColor = ConsoleColor.Gray;
            //            //    ///    }
            //            //    ///    Console.WriteLine(byref);
            //            //    ///    TypeReference tipoResuelto = param.ParameterType.Resolve();
            //            //    ///    Console.WriteLine(" resolveto-> {0}", tipoResuelto);
            //            //    ///}
            //            //    break;
            //            #endregion
            //        }
            //    }
            //}
        }

        void transformMethodReturns(
            TypeDefinition type,
            MethodDefinition md,
            int methodId,
            ParasiRefs tracerReferences
        )
        {
            var worker = md.Body.GetILProcessor();

            foreach (Instruction @return in md.Body.getRets())
            {
                if (md.ReturnType.MetadataType != MetadataType.Void)
                {
                    Console.WriteLine(md.ReturnType);
                }

                md.Body.injectBeforeAndFix(
                    @return,
                    worker.Create(OpCodes.Ldc_I4, methodId),
                    worker.Create(OpCodes.Call, tracerReferences["outFunc"])
                );
            }
        }

        void transformMethodThrows(
            TypeDefinition type,
            MethodDefinition md,
            int methodId,
            ParasiRefs tracerReferences
        )
        {
            var worker = md.Body.GetILProcessor();

            foreach (Instruction @throw in md.Body.getThrows())
            {
                md.Body.injectBeforeAndFix(
                    @throw,
                    worker.Create(OpCodes.Ldc_I4, methodId),
                    worker.Create(OpCodes.Call, tracerReferences["throwFunc"])
                );
            }
        }

        List<FuncTableFileItem> transformAssembly(
            AssemblyDefinition asmDefinition,
            string tracerPath,
            ref int methodId
        )
        {
            var ret = new List<FuncTableFileItem>();
            var parasiRefs = new ParasiRefs(tracerPath, asmDefinition);

            foreach (TypeDefinition type in asmDefinition.MainModule.Types)
            {
                foreach (MethodDefinition md in type.Methods)
                {
                    if (md.HasBody)
                    {
                        md.Body.SimplifyMacros  ();
                        transformMethodStart    (type, md, methodId, parasiRefs);
                        transformMethodReturns  (type, md, methodId, parasiRefs);
                        transformMethodThrows   (type, md, methodId, parasiRefs);
                        md.Body.OptimizeMacros  ();

                        ret.Add(new FuncTableFileItem(
                            methodId,
                            string.Format("{0}::{1}", type.FullName, md.Name)
                        ));

                        methodId++;
                    } // tiene cuerpo :D
                } // para cada método..
            } // para cada tipo ..

            return ret;
        }

        public void setErrorCallback ()
        {
            // TODO: callback para informar errores...
        }
    }
}
